Reactive validation: useValidationState + validationStyles + ajvAdapter (#357)#359
Merged
Conversation
This was referenced Jun 14, 2026
7 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds reactive, whole-document validation to
@json-edit-react/utils:useValidationState,validationStyles,ajvAdapter, and theuseStableValueprimitive they build on. Closes the reactive angle of #357 (origin #197). No core changes — the hooks ride existing public-API channels.The problem
Validating inside a render-time function of
NodeDatathat readsfullData(a style function, anallow*filter, a custom-nodecondition) goes stale under fine-grained re-rendering. Document validity is a whole-document property — an edit at node A can flip the validity of node B on a different branch (JSON Schemaif/then,dependentRequired, discriminated unions) — but B never re-renders when A changes, so its inline validation never re-runs. This is intentional: theReact.memocomparator inmemoNode.tsdeliberately ignoresfullDataidentity. The bug is demonstrated by the existingdevOnly"Validation staleness" demo scratchpad.The approach — identity keyed on the error set
useValidationState(data, validate)runs the validator once perdatachange (O(N), not the O(N²) of validating inside every node), normalizes the issues, and exposes an O(1) query surface:isValid,errors,hasErrorAt,errorsAt,hasErrorWithin.theme/customNodeDefinitions/allow*value on it and valid→valid commits keep the §16 node-memo boundary intact; when validity actually changes, the new identity piercesReact.memothrough that channel and the tree re-renders once — correctly restyling cross-branch nodes.useStableValue(compute, deps, isEqual?)is the general identity-stabilizer underneath, exported for any other cross-branch derived value (duplicate detection, doc-wide totals).What's included
stable-value/—useStableValue(public) + an internal structuraldeepEqual.validation/—useValidationState,validationStyles(theme sugar; leaf slots consulthasErrorAt, opt-inwithinfor collection ancestors),ajvAdapter, and theValidationIssue/ValidationState/Validatetypes.@json-edit-react/utilsminor).devOnlydemo example, Validation flagging, mirroring the staleness scenario but fixed — a direct before/after.Zero-dependency posture
The package adds no runtime dependency, not even on AJV:
ajvAdapteris typed against AJV's error shape structurally, so consumers bring their own validator (or pass any(data) => ValidationIssue[]). The hook also keeps core a type-only dependency — the path-key helper is inlined locally rather than importing core'stoPathStringat runtime.Testing
test/stable-value.test.tsxandtest/validation-state.test.tsx; full suite green (480 passed / 2 todo)..), the §16 identity-stability invariant,ajvAdapternormalization (JSON-Pointer parsing/escapes,required→parent path), andvalidationStylesslot behaviour.JsonEditor): editing one node flips a different-branch node's validity; the far node bails on the commit via the memo boundary, so only the identity-pierce can restyle it. Verified sharp — temporarily freezing the identity check makes both cases fail with the genuine staleness signature (the cross-branch node keeps its stale colour).tsc --noEmitclean; build clean (1.52 kB gzip ESM); noajvin any dependency field or the bundle. Core lint + compile unaffected.Out of scope (follow-ups)
Zod/Yup adapters;
validation.filter()convenience; thecreateUpdateValidatorcommit-gate (shares this kernel); the custom-node glyph component (#358).🤖 Generated with Claude Code